/*
 *  Copyright (c) M2TK Project. All rights reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package m2tk.assistant.app.ui;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.extras.FlatSVGIcon;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import m2tk.assistant.app.kernel.service.StreamAnalyzer;
import m2tk.assistant.app.ui.component.SourceIndicatorButton;
import m2tk.assistant.app.ui.dialog.AboutDialog;
import m2tk.assistant.app.ui.dialog.OpenSourceDialog;
import m2tk.assistant.app.ui.dialog.SourceHistoryDialog;
import m2tk.assistant.app.ui.event.DrawNetworkDiagramRequest;
import m2tk.assistant.app.ui.task.AsyncQueryTask;
import m2tk.assistant.app.ui.task.DrawNetworkDiagramTask;
import m2tk.assistant.app.ui.util.ComponentUtil;
import m2tk.assistant.app.ui.util.MenuItemBuilder;
import m2tk.assistant.app.ui.view.LogsView;
import m2tk.assistant.app.ui.view.StreamInfoView;
import m2tk.assistant.common.*;
import m2tk.assistant.common.event.RefreshInfoViewRequest;
import m2tk.assistant.common.event.ShowInfoViewRequest;
import m2tk.assistant.common.template.DescriptorDecoder;
import m2tk.assistant.common.template.SectionDecoder;
import m2tk.assistant.common.template.SelectorDecoder;
import m2tk.assistant.common.template.TemplateReader;
import m2tk.assistant.common.template.definition.M2TKTemplate;
import m2tk.multiplex.DemuxStatus;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.FrameView;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.fluentui.FluentUiRegularAL;
import org.kordamp.ikonli.fluentui.FluentUiRegularMZ;
import org.kordamp.ikonli.swing.FontIcon;
import org.pf4j.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

import static m2tk.assistant.common.ui.UIConstants.*;

public class MainViewController
{
    private static final Logger log = LoggerFactory.getLogger(MainViewController.class);

    private final FrameView frameView;

    private List<InfoView> infoViews;
    private List<InfoView> coreInfoViews;
    private List<InfoView> pluggedInfoViews;
    private List<Tracer> tracers;
    private LogsView logsView;
    private SourceIndicatorButton sourceIndicatorButton;
    private JPanel cardPane;
    private CardLayout cardLayout;
    private Path lastOpenDirectory;
    private volatile boolean willQuit;
    private volatile boolean pauseRefreshing;

    private ButtonGroup coreInfoViewSwitchButtonGroup;
    private List<AbstractButton> coreInfoViewSwitchButtons;

    private EventBus bus;
    private M2TKDatabase database;
    private StreamAnalyzer analyzer;
    private Timer timer;

    private static final Color DISABLED = UIManager.getColor("Label.disabledForeground");
    private static final int TIMER_INTERVAL_MILLIS = 200;

    public MainViewController(FrameView view)
    {
        frameView = view;
        initUI();
    }

    private void initUI()
    {
        loadInternalTemplates();
        loadPluginsAndExtensions();
        createAndSetupWorkspace();
        setupGlobalAcceleratorKeys();
        setupInitialStates();
    }

    private void loadInternalTemplates()
    {
        TemplateReader reader = new TemplateReader();
        M2TKTemplate psiTemplate = reader.parse(getClass().getResource("/template/PSITemplate.xml"));
        if (psiTemplate != null)
        {
            psiTemplate.getTableTemplates().forEach(SectionDecoder::registerTemplate);
            psiTemplate.getDescriptorTemplates().forEach(DescriptorDecoder::registerTemplate);
            psiTemplate.getSelectorTemplates().forEach(SelectorDecoder::registerTemplate);
        }
        M2TKTemplate siTemplate = reader.parse(getClass().getResource("/template/SITemplate.xml"));
        if (siTemplate != null)
        {
            siTemplate.getTableTemplates().forEach(SectionDecoder::registerTemplate);
            siTemplate.getDescriptorTemplates().forEach(DescriptorDecoder::registerTemplate);
            siTemplate.getSelectorTemplates().forEach(SelectorDecoder::registerTemplate);
        }
    }

    private void loadPluginsAndExtensions()
    {
        // 采用单例模式加载扩展，保证二次加载的插件扩展与首次加载内容一致。
        PluginManager pluginManager = new DefaultPluginManager()
        {
            @Override
            protected ExtensionFactory createExtensionFactory()
            {
                return new SingletonExtensionFactory(this,
                                                     Tracer.class.getName(),
                                                     InfoView.class.getName(),
                                                     StreamObserver.class.getName());
            }
        };

        pluginManager.loadPlugins();
        pluginManager.startPlugins();

        tracers = new ArrayList<>();
        infoViews = new ArrayList<>();
        coreInfoViews = new ArrayList<>();
        pluggedInfoViews = new ArrayList<>();

        List<StreamObserver> streamObservers = new ArrayList<>();
        List<ProgramObserver> programObservers = new ArrayList<>();

        // 加载classpath里的扩展
        List<?> internalExtensions = pluginManager.getExtensions((String) null);
        for (Object extension : internalExtensions)
        {
            // 有可能一个extension实现了不同的扩展接口，所以用if逐个检查。

            if (extension instanceof StreamObserver observer)
            {
                streamObservers.add(observer);
            }
            if (extension instanceof ProgramObserver observer)
            {
                programObservers.add(observer);
            }
            if (extension instanceof Tracer tracer)
            {
                tracers.add(tracer);
            }
            if (extension instanceof InfoView view)
            {
                if (view instanceof LogsView lv)
                    logsView = lv;
                else
                    coreInfoViews.add(view);
                infoViews.add(view);
                view.setupApplication(frameView.getApplication());
            }
        }

        // 加载插件中的扩展
        List<PluginWrapper> plugins = pluginManager.getStartedPlugins();
        for (PluginWrapper plugin : plugins)
        {
            String pluginId = plugin.getPluginId();
            List<StreamObserver> pluginStreamObservers = pluginManager.getExtensions(StreamObserver.class, pluginId);
            streamObservers.addAll(pluginStreamObservers);

            List<ProgramObserver> pluginProgramObservers = pluginManager.getExtensions(ProgramObserver.class, pluginId);
            programObservers.addAll(pluginProgramObservers);

            List<Tracer> pluginTracers = pluginManager.getExtensions(Tracer.class, pluginId);
            tracers.addAll(pluginTracers);

            List<InfoView> pluginViews = pluginManager.getExtensions(InfoView.class, pluginId);
            for (InfoView view : pluginViews)
            {
                infoViews.add(view);
                pluggedInfoViews.add(view);
                view.setupApplication(frameView.getApplication());
            }
        }

        // 针对StreamInfoView做特别的设置
        StreamInfoView view = (StreamInfoView) coreInfoViews.stream()
                                                            .filter(v -> v instanceof StreamInfoView)
                                                            .findFirst()
                                                            .orElseThrow(() -> new IllegalStateException("模块加载错误"));
        view.setStreamObservers(streamObservers);
        view.setProgramObservers(programObservers);
    }

    private void createAndSetupWorkspace()
    {
        cardLayout = new CardLayout();
        cardPane = new JPanel(cardLayout);

        sourceIndicatorButton = new SourceIndicatorButton();
        sourceIndicatorButton.addActionListener(e -> showOpenSourceDialog());

        Container contentPane = frameView.getRootPane().getContentPane();
        contentPane.setLayout(new MigLayout("", "[center][][][][grow][][][]", "[center][grow]"));

        contentPane.add(createClickableLogoLabel());
        contentPane.add(sourceIndicatorButton);
        contentPane.add(createControlButton(false, FluentUiRegularAL.HISTORY_24, e -> reopenInput(), "重新分析"), "gap 5");
        contentPane.add(createControlButton(false, FluentUiRegularAL.DISMISS_24, e -> stopAnalyzer(), "停止分析"), "gap 5");
        contentPane.add(Box.createHorizontalGlue(), "grow");
        contentPane.add(createControlButton(true, FluentUiRegularMZ.PROHIBITED_24, e -> {
            JToggleButton toggle = (JToggleButton) e.getSource();
            if (toggle.isSelected())
                timer.stop();
            else if (analyzer.isRunning())
                timer.start();
            pauseRefreshing = toggle.isSelected();
        }, "暂停刷新"));
        contentPane.add(createControlButton(false, FluentUiRegularAL.ARROW_SYNC_24, e -> manualRefreshing(), "手动刷新"), "gap 5");
        contentPane.add(Box.createHorizontalStrut(20), "wrap");

        contentPane.add(createSideBar(), "grow");
        contentPane.add(cardPane, "span 7, grow");
    }

    private JLabel createClickableLogoLabel()
    {
        JLabel label = new JLabel(new FlatSVGIcon("images/logo-a-round.svg", 42, 40));
        label.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseEntered(MouseEvent e)
            {
                label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
            }

            @Override
            public void mouseExited(MouseEvent e)
            {
                label.setCursor(Cursor.getDefaultCursor());
            }

            @Override
            public void mousePressed(MouseEvent e)
            {
                showAboutDialog();
            }
        });
        return label;
    }

    private AbstractButton createControlButton(boolean toggle, Ikon icon, ActionListener listener, String tooltip)
    {
        AbstractButton button = toggle ? new JToggleButton() : new JButton();
        button.setIcon(getFontIcon(icon, ICON_SIZE_L, LIGHT_BLUE));
        button.addActionListener(listener);
        button.setToolTipText(tooltip);
        button.putClientProperty(FlatClientProperties.STYLE,
                                 """
                                 borderWidth: 0; focusWidth: 0; innerFocusWidth: 0; innerOutlineWidth: 0;
                                 background: $Panel.background; hoverBackground: $Button.toolbar.hoverBackground;
                                 """);
        return button;
    }

    private JToolBar createSideBar()
    {
        coreInfoViewSwitchButtons = new ArrayList<>();
        coreInfoViewSwitchButtonGroup = new ButtonGroup();

        for (InfoView view : coreInfoViews)
        {
            JToggleButton button = new JToggleButton(view.getViewIcon());
            button.setToolTipText(view.getViewTitle());
            button.addActionListener(e -> {
                if (bus != null)
                    bus.post(new ShowInfoViewRequest(view));
            });
            button.putClientProperty(FlatClientProperties.STYLE,
                                     """
                                     toolbar.selectedBackground: $List.selectionBackground;
                                     """);
            coreInfoViewSwitchButtons.add(button);
            coreInfoViewSwitchButtonGroup.add(button);
        }

        JToolBar toolBar = new JToolBar(JToolBar.VERTICAL);
        toolBar.setFloatable(false);
        toolBar.putClientProperty(FlatClientProperties.STYLE, "hoverButtonGroupBackground: null");
        for (AbstractButton button : coreInfoViewSwitchButtons)
        {
            toolBar.add(button);
            toolBar.add(Box.createVerticalStrut(5));
        }
        toolBar.addSeparator();
        toolBar.add(Box.createVerticalStrut(5));

        JPopupMenu popup = new JPopupMenu();
        if (!pluggedInfoViews.isEmpty())
        {
            JMenu pluginViewMenu = new JMenu("扩展视图");
            pluginViewMenu.setIcon(getFontIcon(FluentUiRegularAL.EXTENSION_20, ICON_SIZE_S, LIGHT_BLUE));
            for (InfoView view : pluggedInfoViews)
                view.setupMenu(pluginViewMenu);
            popup.add(pluginViewMenu);
            popup.addSeparator();
        }

        MenuItemBuilder builder = new MenuItemBuilder();
        popup.add(builder.create()
                         .action(e -> showLogsView())
                         .icon(getFontIcon(FluentUiRegularAL.BOOK_PULSE_24, ICON_SIZE_S, LIGHT_BLUE))
                         .text("查看日志")
                         .get());
        popup.add(builder.create()
                         .action(e -> openCalc())
                         .icon(getFontIcon(FluentUiRegularAL.CALCULATOR_20, ICON_SIZE_S, LIGHT_BLUE))
                         .text("打开计算器")
                         .get());
        popup.add(builder.create()
                         .action(e -> openNotepad())
                         .icon(getFontIcon(FluentUiRegularMZ.NOTEPAD_20, ICON_SIZE_S, LIGHT_BLUE))
                         .text("打开记事本")
                         .get());
        popup.add(builder.create()
                         .action(e -> openConsole())
                         .icon(getFontIcon(Feather.TERMINAL, ICON_SIZE_S, LIGHT_BLUE))
                         .text("打开命令行")
                         .get());
        popup.addSeparator();
        popup.add(builder.create()
                         .action(e -> loadCustomTemplates())
                         .icon(getFontIcon(FluentUiRegularAL.LAYER_20, ICON_SIZE_S, LIGHT_BLUE))
                         .text("加载自定义模板")
                         .get());

        toolBar.add(createControlButton(false, FluentUiRegularMZ.OFFICE_APPS_24, e ->
        {
            Component c = (Component) e.getSource();
            popup.show(c, c.getWidth() + 5, 0);
        }, "更多"));

        toolBar.add(Box.createVerticalGlue());
        toolBar.add(createControlButton(false, FluentUiRegularMZ.POWER_24, e -> exitApp(), "退出"));
        return toolBar;
    }

    private void setupGlobalAcceleratorKeys()
    {
        AWTEventListener eventDispatcher = e ->
        {
            if (e instanceof KeyEvent ke && ke.getID() == KeyEvent.KEY_PRESSED)
            {
                // Ctrl-Q
                if (ke.getKeyCode() == KeyEvent.VK_Q &&
                    ke.isControlDown() && !ke.isShiftDown() && !ke.isAltDown())
                {
                    exitApp();
                }

                // Ctrl-O
                if (ke.getKeyCode() == KeyEvent.VK_O &&
                    ke.isControlDown() && !ke.isShiftDown() && !ke.isAltDown())
                {
                    showOpenSourceDialog();
                }

                // Ctrl-F
                if (ke.getKeyCode() == KeyEvent.VK_F &&
                    ke.isControlDown() && !ke.isShiftDown() && !ke.isAltDown())
                {
                    openLocalFile();
                }

                // Ctrl-M
                if (ke.getKeyCode() == KeyEvent.VK_M &&
                    ke.isControlDown() && !ke.isShiftDown() && !ke.isAltDown())
                {
                    openMulticast();
                }

                // Ctrl-L
                if (ke.getKeyCode() == KeyEvent.VK_L &&
                    ke.isControlDown() && !ke.isShiftDown() && !ke.isAltDown())
                {
                    showLogsView();
                }

                // F1
                if (ke.getKeyCode() == KeyEvent.VK_F1 &&
                    !ke.isControlDown() && !ke.isShiftDown() && !ke.isAltDown())
                {
                    showAboutDialog();
                }

                // Alt-1~6
                if ((ke.getKeyCode() == KeyEvent.VK_1 ||
                     ke.getKeyCode() == KeyEvent.VK_2 ||
                     ke.getKeyCode() == KeyEvent.VK_3 ||
                     ke.getKeyCode() == KeyEvent.VK_4 ||
                     ke.getKeyCode() == KeyEvent.VK_5 ||
                     ke.getKeyCode() == KeyEvent.VK_6) &&
                    ke.isAltDown() && !ke.isControlDown() && !ke.isShiftDown())
                {
                    try
                    {
                        coreInfoViewSwitchButtons.get(ke.getKeyCode() - KeyEvent.VK_1)
                                                 .doClick(0);
                    } catch (Exception ignored)
                    {
                        // 如果视图加载失败，则会缺少按钮，导致抛出IndexOutOfBounds异常。
                        // 这里做个保护性措施。
                    }
                }
            }
        };
        Toolkit.getDefaultToolkit().addAWTEventListener(eventDispatcher, AWTEvent.KEY_EVENT_MASK);
    }

    private void setupInitialStates()
    {
        initFileChooserCurrentDirectory();
        timer = new Timer(TIMER_INTERVAL_MILLIS, e -> refreshInfoViews());
        pauseRefreshing = false;
    }

    public void ready()
    {
        AssistantApp application = AssistantApp.getInstance();
        EventBus bus = application.getEventBus();
        M2TKDatabase database = application.getM2TKDatabase();

        for (InfoView view : infoViews)
            view.setupDataSource(bus, database);
        bus.register(this);

        this.bus = bus;
        this.database = database;
        this.analyzer = application.getStreamAnalyzer();

        coreInfoViewSwitchButtons.getFirst().doClick(0);
    }

    @Subscribe
    public void onShowInfoViewRequest(ShowInfoViewRequest request)
    {
        coreInfoViewSwitchButtonGroup.clearSelection();

        InfoView view = request.view();

        int n = coreInfoViews.size();
        for (int i = 0; i < n; i++)
        {
            if (view == coreInfoViews.get(i))
            {
                // 这里不能调用doClick，否则会造成事件死循环。
                coreInfoViewSwitchButtons.get(i).setSelected(true);
                break;
            }
        }

        n = cardPane.getComponentCount();
        for (int i = 0; i < n; i++)
        {
            Component c = cardPane.getComponent(i);
            if (c == view.getViewComponent())
            {
                cardLayout.show(cardPane, view.getViewTitle());
                frameView.getFrame().revalidate();
                return;
            }
        }

        // 目标组件不在CardPane里，则添加该组件。
        JComponent viewComponent = view.getViewComponent();
        cardPane.add(view.getViewTitle(), viewComponent);
        cardLayout.show(cardPane, view.getViewTitle());
        frameView.getFrame().revalidate();
    }

    @Subscribe
    public void onDrawNetworkGraphRequest(DrawNetworkDiagramRequest request)
    {
        ComponentUtil.setWaitingMouseCursor(frameView.getRootPane(), true);
        DrawNetworkDiagramTask task = new DrawNetworkDiagramTask(frameView.getApplication(), database);
        task.execute();
    }

    private void exitApp()
    {
        timer.stop();
        willQuit = true;
        frameView.getApplication().exit();
    }

    private void showAboutDialog()
    {
        AboutDialog dialog = new AboutDialog(frameView.getFrame());
        dialog.setVisible(true);
    }

    private void showOpenSourceDialog()
    {
        if (analyzer == null)
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "内核尚未初始化",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        if (analyzer.isRunning())
            return;

        OpenSourceDialog dialog = new OpenSourceDialog(frameView.getFrame());
        String selectedSourceType = dialog.selectSource();

        switch (selectedSourceType)
        {
            case "本地文件" -> openLocalFile();
            case "组播流" -> openMulticast();
            case "第三方设备" -> openThirdPartyInputSource();
        }
    }

    private void openLocalFile()
    {
        if (analyzer == null)
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "内核尚未初始化",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        if (analyzer.isRunning())
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "分析器正在运行",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setMultiSelectionEnabled(false);
        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        fileChooser.setCurrentDirectory(lastOpenDirectory.toFile());
        fileChooser.setFileFilter(new FileNameExtensionFilter("码流文件（ts/m2ts/mpeg）", "ts", "m2ts", "mpeg"));

        if (JFileChooser.APPROVE_OPTION == fileChooser.showOpenDialog(frameView.getFrame()))
        {
            File file = fileChooser.getSelectedFile();
            String input = file.toURI().toString();

            log.info("开始分析 {}", file);
            boolean started = false;
            try
            {
                started = analyzer.start(input, tracers, this::onDemuxStatusChanged);
            } catch (Exception ex)
            {
                log.error("启动本地文件分析时异常：{}", ex.getMessage());
            }

            if (!started)
            {
                JOptionPane.showMessageDialog(frameView.getFrame(),
                                              "无法启动分析器，详情请查看日志",
                                              "程序异常",
                                              JOptionPane.ERROR_MESSAGE);
            } else
            {
                saveRecentFile(file);
            }
        }
    }

    private void openMulticast()
    {
        if (analyzer == null)
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "内核尚未初始化",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        if (analyzer.isRunning())
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "分析器正在运行",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        String input = JOptionPane.showInputDialog(frameView.getFrame(),
                                                   "组播地址",
                                                   "udp://224.1.1.1:7890");
        if (input == null)
            return;

        if (!isCorrectMulticastAddress(input))
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "无效的组播地址" + System.lineSeparator() + input,
                                          "参数错误",
                                          JOptionPane.ERROR_MESSAGE);
            return;
        }

        boolean started = false;
        try
        {
            started = analyzer.start(input, tracers, this::onDemuxStatusChanged);
        } catch (Exception ex)
        {
            log.error("启动组播流分析时异常：{}", ex.getMessage());
        }

        if (!started)
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "无法启动分析器，详情请查看日志",
                                          "程序异常",
                                          JOptionPane.ERROR_MESSAGE);
        }
    }

    public void openThirdPartyInputSource()
    {
        if (analyzer == null)
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "内核尚未初始化",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        if (analyzer.isRunning())
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "分析器正在运行",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        String input = JOptionPane.showInputDialog(frameView.getFrame(),
                                                   "输入源地址",
                                                   null);
        if (input == null)
            return;

        boolean started = false;
        try
        {
            started = analyzer.start(input, tracers, this::onDemuxStatusChanged);
        } catch (Exception ex)
        {
            log.error("启动外设源分析时异常：{}", ex.getMessage());
        }

        if (!started)
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "无法启动分析器，详情请查看日志",
                                          "程序异常",
                                          JOptionPane.ERROR_MESSAGE);
        }
    }

    private void reopenInput()
    {
        if (analyzer == null)
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "内核尚未初始化",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        if (analyzer.isRunning())
        {
            JOptionPane.showMessageDialog(frameView.getFrame(),
                                          "分析器正在运行",
                                          "警告",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        Supplier<List<String>> query = () -> database.listStreamSourceUris();
        Consumer<List<String>> consumer = sourceUris ->
        {
            SourceHistoryDialog dialog = new SourceHistoryDialog(frameView.getFrame());
            ComponentUtil.setPreferSizeAndLocateToCenter(dialog, 0.5, 0.4);

            String source = dialog.selectFromSourceHistory(sourceUris);
            if (source == null)
                return;

            boolean started = false;
            try
            {
                started = analyzer.start(source, tracers, this::onDemuxStatusChanged);
            } catch (Exception ex)
            {
                log.error("重启分析时异常：{}", ex.getMessage());
            }

            if (!started)
            {
                JOptionPane.showMessageDialog(frameView.getFrame(),
                                              "无法启动分析器，详情请查看日志",
                                              "程序异常",
                                              JOptionPane.ERROR_MESSAGE);
            }
        };
        Consumer<Throwable> failed = t -> log.error("exception: {}", t.getMessage(), t);

        AsyncQueryTask<List<String>> task = new AsyncQueryTask<>(frameView.getApplication(), query, consumer, failed);
        task.execute();
    }

    private void stopAnalyzer()
    {
        if (analyzer != null)
            analyzer.stop();
    }

    private void manualRefreshing()
    {
        bus.post(new RefreshInfoViewRequest());
    }

    private void showLogsView()
    {
        if (bus != null)
            bus.post(new ShowInfoViewRequest(logsView));
    }

    private void openConsole()
    {
        try
        {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command("cmd", "/c", "start", "cmd.exe")
                   .start();
        } catch (IOException ex)
        {
            log.warn("打开命令行程序异常：{}", ex.getMessage());
        }
    }

    private void openCalc()
    {
        try
        {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command("calc.exe").start();
        } catch (IOException ex)
        {
            log.warn("打开计算器程序异常：{}", ex.getMessage());
        }
    }

    private void openNotepad()
    {
        try
        {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command("notepad.exe").start();
        } catch (IOException ex)
        {
            log.warn("打开记事本程序异常：{}", ex.getMessage());
        }
    }

    private void loadCustomTemplates()
    {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setCurrentDirectory(FileUtil.file("template"));
        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        fileChooser.setMultiSelectionEnabled(true);
        fileChooser.setFileFilter(new FileNameExtensionFilter("模板文件（xml）", "xml"));

        if (JFileChooser.APPROVE_OPTION == fileChooser.showOpenDialog(frameView.getFrame()))
        {
            File[] files = fileChooser.getSelectedFiles();
            if (files.length > 0)
            {
                int count = loadUserDefinedTemplates(files);
                if (count == files.length)
                {
                    JOptionPane.showMessageDialog(frameView.getFrame(), "加载成功");
                } else if (count > 0)
                {
                    JOptionPane.showMessageDialog(frameView.getFrame(),
                                                  String.format("有 %d 个模板文件加载失败", files.length - count),
                                                  "请注意",
                                                  JOptionPane.WARNING_MESSAGE);
                } else
                {
                    JOptionPane.showMessageDialog(frameView.getFrame(),
                                                  "全部模板加载失败", "请注意",
                                                  JOptionPane.ERROR_MESSAGE);
                }
            }
        }
    }

    public void setWillQuit()
    {
        willQuit = true;
    }

    private void onDemuxStatusChanged(DemuxStatus status)
    {
        if (willQuit)
            return;

        if (status.isRunning())
        {
            sourceIndicatorButton.updateSourceState(true);
            if (!pauseRefreshing)
                timer.start();
        } else
        {
            sourceIndicatorButton.updateSourceState(false);
            timer.stop();

            JOptionPane.showMessageDialog(frameView.getFrame(), "分析过程结束");
        }
    }

    private boolean isCorrectMulticastAddress(String input)
    {
        try
        {
            URI uri = URI.create(input);
            if (!uri.getScheme().equals("udp"))
                return false;

            return Inet4Address.getByName(uri.getHost()).isMulticastAddress();
        } catch (Exception ex)
        {
            return false;
        }
    }

    private void initFileChooserCurrentDirectory()
    {
        try
        {
            Path pwd = Paths.get(System.getProperty("user.dir"));
            Path recentCfg = pwd.resolve("recent.cfg");
            if (!Files.exists(recentCfg))
            {
                lastOpenDirectory = Paths.get(System.getProperty("user.dir"));
            } else
            {
                String recentFile = FileUtil.readUtf8String(recentCfg.toFile());
                lastOpenDirectory = StrUtil.isNotBlank(recentFile)
                                    ? Paths.get(recentFile)
                                    : Paths.get(System.getProperty("user.dir"));
            }
        } catch (Exception ex)
        {
            log.debug("无法设置文件选择器起始路径：{}", ex.getMessage());
        }
    }

    private void saveRecentFile(File file)
    {
        try
        {
            Path pwd = Paths.get(System.getProperty("user.dir"));
            Path recentCfg = pwd.resolve("recent.cfg");

            FileUtil.writeUtf8String(file.getAbsolutePath(), recentCfg.toFile());
        } catch (Exception ex)
        {
            log.debug("无法保存最近使用的文件：{}", ex.getMessage());
        }
    }

    private void refreshInfoViews()
    {
        if (bus != null)
            bus.post(new RefreshInfoViewRequest());
    }

    private Icon getFontIcon(Ikon ikon, int size, Color color)
    {
        return FontIcon.of(ikon, size, color);
    }

    private int loadUserDefinedTemplates(File[] files)
    {
        TemplateReader reader = new TemplateReader();

        int success = 0;
        for (File file : files)
        {
            if (loadTemplate(reader, file))
                success += 1;
        }
        return success;
    }

    private boolean loadTemplate(TemplateReader reader, File file)
    {
        if (file == null || !file.getName().endsWith(".xml"))
            return false;

        M2TKTemplate userTemplate = reader.parse(file);
        if (userTemplate == null)
            return false;

        userTemplate.getTableTemplates().forEach(SectionDecoder::registerTemplate);
        userTemplate.getDescriptorTemplates().forEach(DescriptorDecoder::registerTemplate);
        userTemplate.getSelectorTemplates().forEach(SelectorDecoder::registerTemplate);
        log.info("加载自定义模板：{}", file);

        return true;
    }
}
